<!DOCTYPE html>
<head>
<title>Test AudioContext constructor with sinkId options</title>
</head>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script>
"use strict";

let outputDeviceList = null;
let firstDeviceId = null;

navigator.mediaDevices.getUserMedia({audio: true}).then(() => {
  navigator.mediaDevices.enumerateDevices().then((deviceList) => {
    outputDeviceList =
        deviceList.filter(({kind}) => kind === 'audiooutput');
    assert_greater_than(outputDeviceList.length, 1,
                        'the system must have more than 1 device.');
    firstDeviceId = outputDeviceList[1].deviceId;

    // Run async tests concurrently.
    async_test(t => testDefaultSinkId(t),
               'Setting sinkId to the empty string at construction should ' +
               'succeed.');
    async_test(t => testValidSinkId(t),
               'Setting sinkId with a valid device identifier at ' +
               'construction should succeed.');
    async_test(t => testAudioSinkOptions(t),
               'Setting sinkId with an AudioSinkOptions at construction ' +
               'should succeed.');
    async_test(t => testExceptions(t),
               'Invalid sinkId arguments should throw an appropriate ' +
               'exception.')
  });
});

// 1.2.1. AudioContext constructor
// https://webaudio.github.io/web-audio-api/#AudioContext-constructors

// Step 10.1.1. If sinkId is equal to [[sink ID]], abort these substeps.
const testDefaultSinkId = (t) => {
  // The initial `sinkId` is the empty string. This will cause the same value
  // check.
  const audioContext = new AudioContext({sinkId: ''});
  audioContext.addEventListener('statechange', () => {
    t.step(() => {
      assert_equals(audioContext.sinkId, '');
      assert_equals(audioContext.state, 'running');
    });
    audioContext.close();
    t.done();
  }, {once: true});
};

// Step 10.1.2~3: See "Validating sinkId" tests below.

// Step 10.1.4. If sinkId is a type of DOMString, set [[sink ID]] to sinkId and
// abort these substeps.
const testValidSinkId = (t) => {
  const audioContext = new AudioContext({sinkId: firstDeviceId});
  audioContext.addEventListener('statechange', () => {
    t.step(() => {
      assert_true(audioContext.sinkId === firstDeviceId,
                  'the context sinkId should match the given sinkId.');
    });
    audioContext.close();
    t.done();
  }, {once: true});
  t.step_timeout(t.unreached_func('onstatechange not fired or assert failed'),
                 100);
};

// Step 10.1.5. If sinkId is a type of AudioSinkOptions, set [[sink ID]] to a
// new instance of AudioSinkInfo created with the value of type of sinkId.
const testAudioSinkOptions = (t) => {
  const audioContext = new AudioContext({sinkId: {type: 'none'}});
  // The only signal we can use for the sinkId change after construction is
  // `statechange` event.
  audioContext.addEventListener('statechange', () => {
    t.step(() => {
      assert_equals(typeof audioContext.sinkId, 'object');
      assert_equals(audioContext.sinkId.type, 'none');
    });
    audioContext.close();
    t.done();
  }, {once: true});
  t.step_timeout(t.unreached_func('onstatechange not fired or assert failed'),
                 100);
};

// 1.2.4. Validating sinkId
// https://webaudio.github.io/web-audio-api/#validating-sink-identifier

// Step 3. If document is not allowed to use the feature identified by
// "speaker-selection", return a new DOMException whose name is
// "NotAllowedError".
// TODO(https://crbug.com/1380872): Due to the lack of "speaker-selection"
// implementation, a test for such step does not exist yet.

const testExceptions = (t) => {
  t.step(() => {
    // The wrong AudioSinkOption.type should cause a TypeError.
    assert_throws_js(TypeError, () => {
      const audioContext = new AudioContext({sinkId: {type: 'something_else'}});
      audioContext.close();
    }, 'An invalid AudioSinkOptions.type value should throw a TypeError ' +
       'exception.');
  });

  t.step(() => {
    // Step 4. If sinkIdArg is a type of DOMString but it is not equal to the
    // empty string or it does not match any audio output device identified by
    // the result that would be provided by enumerateDevices(), return a new
    // DOMException whose name is "NotFoundError".
    // TODO(https://crbug.com/1439947): This does not throw in Chromium because
    // the logic automatically fallbacks to the default device when a given ID
    // is invalid.
    assert_throws_dom('NotFoundError', () => {
      const audioContext = new AudioContext({sinkId: 'some_random_device_id'});
      audioContext.close();
    }, 'An invalid device identifier should throw a NotFoundError exception.');
  });
  t.done();
};
</script>
</html>
